/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the ViewMyCurrency extension.
 *
 * The Initial Developer of the Original Code is
 * Will Moffat.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

//dump("// $Id: WILL_ExchangeRateCache.js,v 1.43 2007/02/26 09:35:24 Will Exp $;\n");

//WILL: should the cache just watch the relevant prefs?

VMC.ExchangeRate = function(fromCurrency,toCurrency,webService) { //WILL: TODO: fix inconsistant OOP style
  var STATUS_OK      = "OK";      // present, well-formed and not old
  var STATUS_OLD     = "OLD";     // OK but needs web-update
  var STATUS_INVALID = "INVALID"; // missing, or not well-formed (0)

  var SEPARATOR      = '-';
  
  if (webService===undefined) { VMC.log('ERR',"Missing web service for ExchangeRate: "+fromCurrency+" - "+toCurrency); }

  this.fromCurrency  = fromCurrency;
  this.toCurrency    = toCurrency;
  this.rate          = NaN;
  this.isCalculated  = false; // does toCurrency or fromCurrency have a formula?
  this.status        = STATUS_INVALID; //until my rate is set
  this.lastGoodUpdateTime = 0; //not updated yet
  this.lastUpdateErr  = null;
  this.webService    = webService;
  this._init = _init;
  
  this._init();

  function _init() {
    if (this.fromCurrency.formula || this.toCurrency.formula) {
      this.isCalculated = true;
    }
  }
      

//this.toString = function()     { return toCurrency.code+'/'+fromCurrency.code+' ('+this.status+')' };

  // Assumption: the intermediate currency is the same
  this.getDependentCurrency = function() {
    if (!this.isCalculated) { VMC.log('ERR',this+' is not calculated'); }
    return this.fromCurrency.formula ? this.fromCurrency.getFormulaCurrencyCode() : this.toCurrency.getFormulaCurrencyCode();
  };
    
  this.toString = function(encoded) {
    if (!encoded) { //human readable
      var rateStr = this.fromCurrency.code+SEPARATOR+this.toCurrency.code;
      return rateStr;
    }
    else { // computer readable
      return [this.toCurrency.code, 
	      this.fromCurrency.code,
	      this.rate,
	      this.webService.getName(),
	      this.lastGoodUpdateTime ].join(SEPARATOR); //datetime in ms
   }
  };
  this.updateFromStr = function(str) {
    var bits=str.split(SEPARATOR);
    if (bits.length != 5)                       { VMC.log('ERR','malformed rate: not 6 bits in str ['+str+']'); return; }
    if (this.toCurrency.code != bits.shift())   { VMC.log('ERR','malformed rate: missing To Code in str ['+str+']'); return; } //WILL: does it really make sense to store these twice?
    if (this.fromCurrency.code != bits.shift()) { VMC.log('ERR','malformed rate: missing From code in str ['+str+']'); return; }
    this.setRate(bits.shift()); //update status as well
    this.webService = VMC.webServiceList.getServiceFromName(bits.shift());
    this.lastGoodUpdateTime = Number(bits.shift()).valueOf();
  };

  this.isStatusInvalid  = function()    { return this.status==STATUS_INVALID; };
  this.isStatusOld      = function()    { return this.status==STATUS_OLD; };
  this.isStatusOK       = function()    { return this.status==STATUS_OK; };
  this.setStatusOK      = function()    { this.status=STATUS_OK; this.lastUpdateErr  = null; };
  this.setStatusInvalid = function()    { this.status=STATUS_INVALID; };
  this.setStatusOld     = function()    { this.status=STATUS_OLD; };

  
  this.setRate  = function(rate) {
    this.rate=rate;
    if (VMC.ExchangeRate.isSaneValue(rate)) { this.status=STATUS_OK;      }
    else                                     { this.status=STATUS_INVALID; }
  };

  this.removeUpdateError = function() { this.lastUpdateErr=null; };

  this.setWebUpdate = function(success,err) {
    // this just records if the upate was successful or not
    // the status will be changed by setRate or by WILL: TODO: checkNotOld
    if (success) { this.lastGoodUpdateTime = new Date().getTime(); }
    else         { this.lastUpdateErr=err;               }
  };
  this.getRate = function() {
    if (this.status == STATUS_INVALID) { VMC.log('INFO','tried to use invalid ex rate '+this); }
    return this.rate;
  }; 
  this.getLastGoodUpdateTime = function() { 
    if (!this.lastGoodUpdateTime) { return 'Never successfully updated'; }
    else                          { return new Date(this.lastGoodUpdateTime).toLocaleString(); }
  }; 
  
  // sets STATUS_OLD and returns true is we haven't been updated in X hours
  this.setOldIfLastUpdateInHours = function(ageLimit) {
    if (!this.lastGoodUpdateTime) {this.setStatusOld(); return true;}

    var oldTime = this.lastGoodUpdateTime;
    var nowTime = new Date().getTime();
    var ageInHours= (( nowTime - oldTime ) / (1000*60*60)) ;

    if (ageInHours > ageLimit ) {
      this.setStatusOld();
      return true; // it is old
    }
    return false;
  };

  this.getUpdateUrl = function() {
    VMC.webServiceList.assertHasWebService(this);
    return this.webService.getUpdateUrl(fromCurrency.code, toCurrency.code);
  };

};

// class function
VMC.ExchangeRate.applyToEachRate = function(sourceCurrList, targetCurrList, func) {
    if (!sourceCurrList || !sourceCurrList.items || !sourceCurrList.items.length) { VMC.log('ERR','Missing source list'); }
    if (!targetCurrList || !targetCurrList.items || !targetCurrList.items.length) { VMC.log('ERR','Missing target list'); }
    if (!func) { VMC.log('ERR','Missing func!'); }
    
    var sourceCurr;
    var targetCurr;
    var rateIndex=0;
    for (var s=0;s<sourceCurrList.items.length;s++) {
      sourceCurr=sourceCurrList.items[s];
      for (var t=0;t<targetCurrList.items.length;t++) {
	targetCurr=targetCurrList.items[t];
	func(sourceCurr,targetCurr); // apply the func
      }
    }
  };

// would this string or float be acceptable for an exchange rate?
VMC.ExchangeRate.isSaneValue = function(rateValueToCheck) {
  try {
    var rate = VMC.ExchangeRate.parseValue(rateValueToCheck);
  } catch (e) {
    return false; // no it's not a good value
  }
  return true; // didn't get an exception, so it must be OK
};
  
  
// return a float representing this exchange rate number (accepts string or floats)
// throws an exception if value is invalid
VMC.ExchangeRate.parseValue = function(rateValueToCheck) {
  var MAX_EXCHANGE_RATE = 80000; // 1.00 KWD Kuwait Dinars = 55,202.77 VND Vietnam Dong
  // see   http://en.wikipedia.org/wiki/Highest_valued_currency_unit
  // and   http://en.wikipedia.org/wiki/Least_valued_currency_unit
  var rateValue=rateValueToCheck;
  if (typeof(rateValueToCheck) == 'string') { rateValue=parseFloat(rateValueToCheck); }
  if (typeof(rateValue)        != 'number') { throw 'Invalid rateValue: ['+rateValueToCheck+'] - not a number'; }
  if ( !(rateValue>0 && rateValue<MAX_EXCHANGE_RATE) )  { 
    throw 'Looks like an invalid rate: ['+rateValue+'] should be greater than 0 and less than '+MAX_EXCHANGE_RATE;  }
  return rateValue;
};
 


VMC.ExchangeRateCache = function(prefReadRateFunc, prefWriteRateFunc, sourceCurrencyList, targetCurrencyList) {
  var self=this;

  //This is just a cached copy of values stored in GM preferences. Access using getRate
  this.exchangeRates = []; // This is never emptied, so it can contain rateObjs no longer in use if source or target lists change
  this._prefReadRateFunc         = prefReadRateFunc;
  this._prefWriteRateFunc        = prefWriteRateFunc;

  this.webUpdater = new VMC.WebUpdater(this);

  // functions //
  this.sourceOrTargetListChanged = sourceOrTargetListChanged;
  this.setMissingRateWebServices = setMissingRateWebServices;
  this.getRate              = getRate;
  this.getRateObj           = getRateObj;
  this.setAllRatesFromPrefs = setAllRatesFromPrefs;
  this.setAllCalculatedRates = setAllCalculatedRates;
  this.updateFromWeb        = updateFromWeb;
  this.listOKwebServices    = listOKwebServices;
  this.setAllRatesToWebService = setAllRatesToWebService;
  this.toString                  = toString;

  // private functions //
  this._getNumRates = _getNumRates;
  this._applyToEach = _applyToEach;
  this._checkForOldRates=_checkForOldRates;
  this._getDefaultWebService = _getDefaultWebService;
  this._getKey=_getKey;
  this._setRate=_setRate;
  this._getAllRateObjs=_getAllRateObjs;
  this._calculateRateFromFormula = _calculateRateFromFormula;

  this._setFromXmlCallback = _setFromXmlCallback; // used by the WebUpdater
  this._setFailureCallback = _setFailureCallback;

  //private vars
  this.missingRates=[]; //key is provided by _getKey


  this.UPDATE_INTERVAL=8; //in hours

  this.sourceCurrencyList=sourceCurrencyList; 
  this.targetCurrencyList=targetCurrencyList;
  this.sourceOrTargetListChanged();

  // for given list of rates and webservices say if they work or not - only used in testing right now
  // if rates missing then do all
  // if webservices missing then do all
  function listOKwebServices(rates,services) {
    if (!rates)    {rates=this._getAllRateObjs(); }
    if (!services) {services=VMC.webServiceList; }

     for (var r=0; r<rates.length; r++) { 
       var rateObj=rates[r];
       dump('testing: '+rateObj+'\t');
       for (var w=0;w<services.length;w++) {
 	var webService=services[w];
 	dump('with '+webService.getName()+'=');
 	var success=this.webUpdater.test(rateObj,webService);
	dump(success+'\t');
       }
       dump('\n');

     }
   }

  function toString() {
    var str='** ExchangeRateCache = ';
    var formatRate = function(rateObj) { return rateObj.toString()+':'+rateObj.getRate();   };
    this._applyToEach( function(r) {str+=formatRate(r)+', ';} );
    str+=" **";
    return str;
  }



  // returns true if all rates could be set without requiring immediate webupate
  function sourceOrTargetListChanged() {
    var allRatesSet = this.setAllRatesFromPrefs();
    this.setMissingRateWebServices(); // WILL: TODO: what to do with success return value?
    this._checkForOldRates();
    return allRatesSet;
  }    

  // returns rate: 1 fromCurrency in toCurrency units   e.g. getRate(BEF,EUR) --> 1/40  as 1 BEF = 1/40 EUR
  // this won't work until the rates have been updated from prefs
  function getRate(fromCurrency,toCurrency) {
    if (fromCurrency.equals(toCurrency)) { return 1; } //we don't bother with Exchange Rate objects for the obvious!

    var ex=this.getRateObj(fromCurrency,toCurrency);
    if (!ex) { return null; }
    return ex.getRate();
  }
  function getRateObj(fromCurrency,toCurrency, createMissing) {
    if (fromCurrency.equals(toCurrency)) { return 1; } //NB this is not a rateObj!
    var key = toCurrency.code+'/'+fromCurrency.code; //WILL: check - we aren't using _getKey because we don't have a rateObj yet. Is this sensible?
    var ex=this.exchangeRates[key];
    if (!ex) {
      if (createMissing) {
	ex = new VMC.ExchangeRate(fromCurrency,toCurrency,null); //no webservice defined for this rate yet
	this.exchangeRates[key]=ex;
      } else {
	//we weren't told to create missing objects - so return null
	VMC.log('DBG','could not find rate '+key+' in ExchangeRateCache');
	return null;
      }
    }
    return ex;
  }

  //////////////////////////////////////////////////////////////////////////
  // All the rest of the code is for keeping the exchange rate up to date //
  //////////////////////////////////////////////////////////////////////////

  function _getAllRateObjs() {
    var target_list=this.targetCurrencyList.items;
    var source_list=this.sourceCurrencyList.items;
    var toCurrency, fromCurrency;
    var rates=[];
    var rateObj;

    for (var t=0; t<target_list.length; t++) {
      toCurrency=target_list[t];
      for (var v=0; v<source_list.length; v++) {
	fromCurrency=source_list[v];
	if (fromCurrency.equals(toCurrency)) { continue; }//we don't bother with Exchange Rate objects for the obvious!
	rateObj=this.getRateObj(fromCurrency,toCurrency, true); //create any missing objs
	rates.push(rateObj);
      }
    }
    return rates;
  }

  function _checkForOldRates() {
    var oldRates=[];
    var allRates=this._getAllRateObjs();
    for (var i=0; i<allRates.length; i++) { 
      if (allRates[i].setOldIfLastUpdateInHours(this.UPDATE_INTERVAL)) {
	oldRates.push(allRates[i]);
      }
    }
    return oldRates;
  }

  function setAllCalculatedRates() {
    var secondPassFunc = function(rateObj) {
      if (rateObj.isCalculated) {
	self._calculateRateFromFormula(rateObj); // update from formula (fail with error)
      }
    };
    this._applyToEach( secondPassFunc );
  }

 // returns true if we found all rates
  // works in 2 passes, (2nd pass is for calculated rates. WILL: TODO: that means only one level of redirection is allowed)
  function setAllRatesFromPrefs() {
    if (!this._getNumRates()) { return true; } // source and/or target list are empty

    var foundAllRates=true;

    var firstPassFunc = function(rateObj) {
      var key = self._getKey(rateObj);
      if (!rateObj.isCalculated) {
	var success=self._prefReadRateFunc(rateObj,true); // update from prefs (fail silently)
	if (!success) {foundAllRates=false; }
      }
      self.exchangeRates[key]=rateObj; // update the hash (does not do anything unless rate was new) 
    };

    this._applyToEach( firstPassFunc );
    this.setAllCalculatedRates();


    return foundAllRates; //WILL: TODO: this is used by OptionsDialogManager to decide if an update is necessary. Maybe ExchangeRateCache should automatically update?
  }

  // We insist the intermediateCurrency is in both source and target currency lists, reasons:
  // Target list: we need the rate + it's good for people to be see EUR prices these days!
  // Source list: we need the rate + it's unlikely you would want to convert GBP to BEF but not EUR to BEF too
  function _calculateRateFromFormula(rateObj) {

    var setRateFunc = function(rate) {
      rateObj.setRate(rate);
      rateObj.removeUpdateError();
      rateObj.setWebUpdate(true); //success WILL: TODO: shouldn't this all be inside rateObj class?
    }

    var rate = 1; // this is what we are trying to calculate

    var toCurrency   = rateObj.toCurrency;
    var fromCurrency = rateObj.fromCurrency;
 
    if (!rateObj.isCalculated) { VMC.log('ERR','Rate '+rateObj+' is not calculated'); }
    if (!toCurrency.formula && !fromCurrency.formula) { VMC.log('ERR','not formula in either '+rateObj); }
    if (toCurrency==fromCurrency) { setRateFunc(1); return; }

    // example:
    // ATS-DEM
    // =   (ATS-EUR) * (EUR-DEM)
    // = 1/(EUR-ATS) * (EUR-DEM)
    
    var intermediateCode  = null; // intermediate currency code e.g. EUR
    var secondDependantCode = null; 
    var intermediateCurrency = null;
    if (fromCurrency.formula) { intermediateCode = fromCurrency.getFormulaCurrencyCode(); }
    if (toCurrency.formula)   {
      var secondIntermediateCode = toCurrency.getFormulaCurrencyCode();
      if (!intermediateCode) { intermediateCode = secondIntermediateCode; }     
      if ( intermediateCode != secondIntermediateCode) { VMC.log('ERR','intermediate codes are different'); }
    }

    intermediateCurrency = this.targetCurrencyList.findFromCode(intermediateCode);

    //WILL: TODO: ? do division last to preserve accuracy. WILL: I don't think it makes a difference

    if (fromCurrency.formula) {
      rate = rate / fromCurrency.getFormulaRate();  // inverse 
    } else {
      rate = rate * this.getRate(fromCurrency,intermediateCurrency);
    }

    if (toCurrency.formula) {
      rate = rate * toCurrency.getFormulaRate();
    } else {
      rate = rate * this.getRate(intermediateCurrency,toCurrency); // We insist intermediateCurrency is in the source list. 
      // NOT USED: rate = rate / this.getRate(toCurrency,intermediateCurrency); //inverse because intermediate currency (EUR) might not be in the source list. But we insist that it's in the target list
    }


    setRateFunc(rate);
  }
    
    


  function setAllRatesToWebService(webService) {
    this._applyToEach( function(rateObj) {rateObj.webService=webService;} );
  }

  //////////////////////////
  //private from here on: //
  //////////////////////////

  function _getNumRates() {
    var allRates=this._getAllRateObjs();    
    return allRates.length;
  }

  function _applyToEach(func) {
    var allRates=this._getAllRateObjs();    
    if (!func) { VMC.log('ERR','no func'); }
    if (!allRates || !allRates.length) { VMC.log('ERR','no rate objs to apply'); }
    
    var rateObj;
    for (var i=0; i<allRates.length; i++) {
      rateObj = allRates[i];
      func(rateObj); // apply the func
    }
  }
    

  function _getKey(rateObj) {
    if (!rateObj) { VMC.alert('Missing rate obj'); }
    return rateObj.toCurrency.code+'/'+rateObj.fromCurrency.code;
  }

  function _setRate(rateObj,rate) {
    var key = this._getKey(rateObj);
    var ex=this.exchangeRates[key];
    if (!ex) { VMC.alert('tried to get missing exchange rate '+key); }
    if (ex !== rateObj) { VMC.log('ERR','ExchangeRateCache rate: '+ex+' and _setFromXmlCallback rate:'+rateObj+' are not the same'); }
    ex.setRate(rate); //update the cached copy
    this._prefWriteRateFunc(rateObj);  //update the pref copy (this is a write-through cache)
    VMC.log('DBG3','Set rate: '+key+' = '+rate);
  }

  
  // returns errorStr if there was a problem
  function _setFromXmlCallback(item,xml) {
    try {
      var rate=item.webService.getRateFromContent(xml); //performs sanity checking

      // SUCCESS!
      item.setWebUpdate(true);
      self._setRate(item,rate);

    } catch (e) {
      return e.toString();
    }
    return undefined; // no error
  }
  function _setFailureCallback(item,errorStr) {
    item.setWebUpdate(false,errorStr);
    // alert('got failure for '+item+' error='+errorStr);
  }

  function updateFromWeb(forceUpdate,stopOnFirstFailure) {
    if (this.webUpdater.isBusy) {
      VMC.log('WARN','Webupdater is already busy - not starting');
      return;
    }
    var todo;
    if (forceUpdate) { todo = this._getAllRateObjs();   }
    else             { todo = this._checkForOldRates(); }

    todo.grep = function(conditionFunc) { //WILL: TODO: make available as WArray.prototype.grep ??
      var items=[];
      for (var i=0;i<this.length;i++) {
	if (conditionFunc(this[i])) {
	  items.push(this[i]);
	}
      }
      return items;
    };
    todo = todo.grep( function(rateObj) { return !rateObj.isCalculated; } ); // remove calculated rates from the list to update

    if (todo.length) { this.webUpdater.start(todo,this._setFromXmlCallback,this._setFailureCallback,stopOnFirstFailure); }
    else             { VMC.log('INFO','Web update skipped - no rates require update'); }
  }


  function setMissingRateWebServices() {
    var success = true;
    var allRates=this._getAllRateObjs();
    for (var i=0; i<allRates.length; i++) { 
      var rateObj=allRates[i];
      if (!rateObj.webService && !rateObj.isCalculated) {
	var service=this._getDefaultWebService(rateObj);
	if (!service) {
	  success = false;
	  VMC.log('WARN','rate: '+rateObj+' has no default web service');
	} else {
	  VMC.log('INFO','rate: '+rateObj+' default webservice = '+service.getName());
	  rateObj.webService=service;
	}
      }
    }
    return success;
  }

  function _getDefaultWebService(rateObj) {

    function supportsCodes(service,rateObj) {
      var fromCode=rateObj.fromCurrency.code;
      var toCode=rateObj.toCurrency.code;

      var contains = function(array,item) { return (array.indexOf(item) != -1); };

      var containsCodeTo   = contains(service.supportedCodes,toCode);
      var containsCodeFrom = contains(service.supportedCodes,fromCode);

      if (containsCodeTo && containsCodeFrom) { return true; }
      return false;
    }

    // go through all the web services
    for (var i=0; i<VMC.webServiceList.length; i++) {
      var service=VMC.webServiceList[i];
      if (supportsCodes(service,rateObj)) { return service; }
    }

    return null;
  } 



};

VMC.ExchangeRateCache.isValid = function(cache,failSilently) {
  var error;
  if      (!cache)              { error='Null cache'; }
  else if (!VMC.CurrencyList.prototype.isValid(cache.sourceCurrencyList,failSilently)) { error='Empty source list'; }
  else if (!VMC.CurrencyList.prototype.isValid(cache.targetCurrencyList,failSilently)) { error='Empty target list'; }

  if (error && !failSilently) { VMC.log('ERR',error); }
  if (error) { return false; }
  return true;
};

